缓存菜品、套餐
- 需求:刚刚做的C端商品浏览功能的四个接口,是用户端访问频率最高的(每次点开APP首页都会调用)。
- 建议:考虑引入Redis缓存。
- 理由:菜品分类和套餐信息不经常变动,直接查询数据库会产生不必要的IO开销。使用Redis缓存这些List结果,可以极大地提升首页加载速度。
1.1 缓存菜品
- 目前的难点:在Redis中以什么数据类型(String (❌️) Hash List Set Zset,如果可以组合,Hash来存DishVO,然后再统一由一个List存起来就好了)缓存这些菜品(List<DishVO>),缓存的有效期设置成多少。
- 大致流程:先去Redis中查,如果没有,再去MySQL中查,再同步写入到Redis中,并且为了防止缓存雪崩,各个菜品的有效期需要加随机数,防止菜品在Redis中同一时刻全部失效。再延伸到某个极热点的数据,需要设置永久ttl或者逻辑永久。再思考:系统的并发量实际上达不到这样的级别,但是如果成本不高,可以防患于未然。
- 需要注意的问题:当增删改菜品时,需要对Redis的数据同步修改
AI review:
| 维度 | 你的思路 | 优化建议 |
|---|---|---|
| 数据类型 | 倾向于 Hash/List 组合 | String (JSON),将 List<DishVO> 直接序列化为 JSON 字符串,以分类 ID 为 Key,简单高效。 |
| 更新策略 | 同步修改 Redis | 删除 Redis 对应 Key,让下一次请求触发回写。 |
| 一致性 | 仅修改菜品时考虑 | 删除、状态切换(起售/停售) 均需清理缓存。 |
| 实现方式 | 手动硬编码逻辑 | 推荐尝试 Spring Cache 注解,事半功倍。 |
新的难点:如何将菜品数据序列化/反序列化
这里我们在做Redis配置类的时候,已经配置了Redis的序列化器
java// 设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置value的序列化器,使用 GenericJackson2JsonRedisSerializer GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);Redis的序列化器会自动帮我们做好Java对象的序列化与反序列化,所以直接取用即可
目前的问题,无法处理LocalDateTime这类Java8新类型
修改:
java// 1. 使用自定义对象映射器 JacksonObjectMapper jacksonObjectMapper = new JacksonObjectMapper(); // 2. 关键:虽然你的类处理了时间,但为了让 Redis 能反序列化回 List<DishVO>, // 依然需要开启类型保留(Type Id) jacksonObjectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); // 3. 将对象映射器传给序列化器 GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(jacksonObjectMapper); // 设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置value的序列化器,使用 GenericJackson2JsonRedisSerializer redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer); return redisTemplate;
实现增删改菜品操作时清理Redis缓存
增加菜品
javaredisTemplate.delete("dish_" + dishDTO.getCategoryId());删除菜品
javaredisTemplate.delete(redisTemplate.keys("dish_*"));修改菜品时就要注意了,如果修改的是菜品的分类,那么原分类的缓存和新分类的缓存都需要清掉
这里不需要做这样的逻辑判断,直接全部清除掉即可
1.2 缓存套餐
1.2.1 Spring Cache
Spring Cache,是一个框架,对缓存技术的封装,实现以注解的形式实现缓存功能。底层可以切换不同的缓存实现,如:EHCache、Caffeine、Redis。
使用
导入Maven依赖,根据你导入的缓存技术的依赖,决定其底层具体实现的缓存技术。比如你导入的是Redis的坐标,导入Spring Cache的坐标后,其底层实现的就是Redis。
xml<!-- Spring Cache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency>常用注解:
注解 作用 对应 Redis 操作 典型应用场景 @EnableCaching开启缓存注解功能,通常加在启动类或Redis配置类上 @Cacheable查缓存:有则返回,无则执行方法并存入缓存 GET+SETC 端查询菜品/套餐列表 @CachePut更新缓存:执行方法并将结果强制放入缓存 SET管理端修改菜品后同步更新 @CacheEvict清理缓存:从缓存中删除指定数据 DEL管理端删除菜品或停售菜品 @Caching组合注解:可以同时触发多个缓存操作 组合操作 一个操作影响多个分类缓存时 @CacheConfig类级共享配置:抽取统一的缓存名称 N/A 在类头声明 setmealCache注解属性值: cacheNames = "userCache",key = "#result.id"。最后存在Redis中的key为 userCache::id
key属性的值有多种写法,使用spel (Spring Expression Language)
key = "#user.id" ,
user表示入参,名称需要和参数名保持一致key = "#result.id",
result表示返回值,固定写法key = "#p0.id",
p0表示第一个参数,p1表示第二个参数
推荐使用第一种,可读性高。==注意:不同的注解支持key属性的写法不同==,比如@Cacheable就不支持result写法
写法:
java@Cacheable(cacheNames = "userCache",key = "#id")java@CachePut(cacheNames = "userCache",key = "#user.id")java@CacheEvict(cacheNames = "userCache",key = "#id")java@CacheEvict(cacheNames = "userCache",allEntries = true) // 清理所有userCache开头的key
1.2.2 实现思路
- 导入Maven坐标
- 在Redis配置类中添加Spring Cache管理器,指定Spring Cache序列化和反序列化的规则
- 启用@EnableCaching
- 在用户端查询套餐接口上加上@Cacheable注解
- 在管理端增删改套餐接口上加上@CachePut、@CacheEvict注解
1.2.3 开发
导入Maven坐标
配置Spring Cache管理器,在原来的基础上添加CacheManager方法,返回该Bean。并启用SpringCache
java/** * redis配置类 */ @EnableCaching @Configuration @Slf4j public class RedisConfiguration { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { log.info("开始构建Redis缓存管理器..."); // 1. 构造和你 redisTemplate 一模一样的 ObjectMapper JacksonObjectMapper jacksonObjectMapper = new JacksonObjectMapper(); jacksonObjectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); // 2. 使用这个 ObjectMapper 创建序列化器 GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(jacksonObjectMapper); // 3. 配置 RedisCacheConfiguration RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 设置 Key 的序列化方式为 String .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置 Value 的序列化方式为 JSON(使用你自定义的规则) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // 【建议】设置默认过期时间,例如 1 小时,防止缓存永久堆积 .entryTtl(Duration.ofHours(1)) // 不缓存空值 .disableCachingNullValues(); // 4. 构建并返回 CacheManager return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } }修改用户端,注意:Spring Cache注解一般写在Service层,可以保证业务逻辑的复用性。但有需要注意的"==大坑==",==类内部方法调用导致缓存失效==。
原因:Spring Cache 是基于 动态代理 的。只有当外部对象调用 Service 时,才会经过 Spring 的代理对象,从而触发缓存逻辑。如果是“类内部自调用”,执行的是原始对象的方法,注解会直接被忽略。
错误示例:
java@Service public class DishServiceImpl implements DishService { public List<DishVO> getById(Long id) { // 直接调用本类带注解的方法 return this.listWithCache(id); // ❌ 此时 @Cacheable 会失效! } @Cacheable(cacheNames = "dish", key = "#id") public List<DishVO> listWithCache(Long id) { return dishMapper.getById(id); } }
在进行管理端套餐开发的时候,遇到了一个很隐蔽的问题,经过排查,发现在保存套餐的时候,上一步向套餐表插入信息的时候,没有进行主键回填,导致插入套餐对应菜品的时候,套餐ID为空。新增套餐时进行主键回填,解决了新增套餐插入套餐菜品映射表的问题。
现在又遇到问题,修改套餐表的时候,前端并未传入菜品套餐表的套餐ID,需要手动设置,已成功解决。
现在进行套餐缓存开发,将用户端套餐查询操作@Cacheable打开,客户端增删改操作同步修改缓存
问题:客户端增删改操作对缓存操作的粒度如何控制